<template>
  <h1>大文件上传</h1>
  <input type="file" @change="handleFileChange" />
  <el-button @click="handleUpload">上传</el-button>
  <el-button @click="handlePause" v-if="isPaused">暂停</el-button>
  <el-button @click="handleResume" v-else>恢复</el-button>
  <div v-show="hashPercentage > 0">
    <h3>计算文件的hash</h3>
    <el-progress :percentage="hashPercentage"></el-progress>
    {{ '计算完成，文件hash为：' + hash }}
  </div>
  <div v-show="uploadPercentage > 0">
    <h3>大文件上传总进度</h3>
    <el-progress :percentage="fakeUploadPercentage"></el-progress>
  </div>
  <div v-show="chunkList.length > 0">
    <h3>分片上传进度</h3>
    <el-table :data="chunkList" style="width: 100%">
      <el-table-column prop="chunkHash" label="分块" width="270"> </el-table-column>
      <el-table-column prop="size" label="size(Kb)" width="90" :formatter="(row, column, value) => Math.floor(value / 1024)"> </el-table-column>
      <el-table-column prop="percentage" label="上传进度">
        <template #default="scope">
          <el-progress :percentage="scope.row.percentage"></el-progress>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
const SIZE = 3 * 1024 * 1024; // 定义切片的大小
import SparkMD5 from 'spark-md5';
const Status = { wait: 1, error: 2, done: 3, fail: 4 };

export default {
  data() {
    return {
      // 上传的文件
      file: null, // 初始值需要是 null  空对象 if判断的时候是true
      // 文件分片之后  每一个片
      chunkList: [],
      worker: null,
      hash: '',
      hashPercentage: 0,
      fakeUploadPercentage: 0,
      requestList: [],
      isPaused: true,
    };
  },
  computed: {
    // 针对每一个 chunk的进度 计算出总的上传进度
    uploadPercentage() {
      if (!this.file || !this.chunkList.length) return 0;
      const loaded = this.chunkList.map((item) => item.size * item.percentage).reduce((acc, cur) => acc + cur);
      return parseInt((loaded / this.file.size).toFixed(2));
    },
  },
  watch: {
    uploadPercentage(now) {
      if (now > this.fakeUploadPercentage) {
        // 文件进度是这个，在用户暂停之后 进度条会倒退 在这里保证进度条不到退，给用户更好的体验
        this.fakeUploadPercentage = now;
      }
    },
  },
  methods: {
    // 判断文件是否可以秒传
    async verifyUpload(filename, fileHash) {
      const { data } = await this.request({
        url: 'http://localhost:8080/verify',
        method: 'post',
        headers: { 'content-type': 'application/json' },
        data: JSON.stringify({ filename, fileHash }),
      });
      return JSON.parse(data).data;
    },
    // 每一个 chunkList设置 他的百分比
    createProgressHandler(item) {
      return (e) => {
        item.percentage = parseInt(String((e.loaded / e.total) * 100));
      };
    },
    // 使用 web-worker 计算 hash
    calculateHash(fileChunkList) {
      return new Promise((resolve) => {
        // 添加 worker 属性
        // this.worker = new Worker('/hash.js');
        // this.worker.postMessage({ fileChunkList });
        // this.worker.onmessage = (e) => {
        //   const { percentage, hash } = e.data;
        //   this.hashPercentage = percentage;
        //   if (hash) {
        //     resolve(hash);
        //   }
        // };
        const spark = new SparkMD5.ArrayBuffer();
        const reader = new FileReader();
        const file = this.file;
        // 文件大小
        const size = this.file.size;
        let offset = 2 * 1024 * 1024;
        let chunks = [file.slice(0, offset)];
        // 前面100K
        let cur = offset;
        while (cur < size) {
          // 最后一块全部加进来
          if (cur + offset >= size) {
            chunks.push(file.slice(cur, cur + offset));
          } else {
            // 中间的 前中后去两个字节
            const mid = cur + offset / 2;
            const end = cur + offset;
            chunks.push(file.slice(cur, cur + 2));
            chunks.push(file.slice(mid, mid + 2));
            chunks.push(file.slice(end - 2, end));
          }
          // 前取两个字节
          cur += offset;
        }
        // 拼接
        reader.readAsArrayBuffer(new Blob(chunks));
        reader.onload = (e) => {
          spark.append(e.target.result);
          this.hashPercentage = 100;
          resolve(spark.end());
        };
      });
    },
    // 对原生的 XMLHttpRequest 进行了一层封装
    request({ url, method = 'post', data, headers = {}, onProgress = (e) => e, requestList = [] }) {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        // 监听上传进度
        xhr.upload.onprogress = onProgress;
        xhr.open(method, url);
        Object.keys(headers).forEach((key) => xhr.setRequestHeader(key, headers[key]));
        xhr.send(data);
        xhr.onload = (e) => {
          if (requestList) {
            // 将请求成功的 xhr 从列表中删除
            const xhrIndex = requestList.findIndex((item) => item === xhr);
            requestList.splice(xhrIndex, 1);
          }
          resolve({ data: e.target.response });
        };
        xhr.onreadystatechange = function () {
          if (xhr.readyState === 4) {
            if (xhr.status === 200) {
              // 响应成功
            } else {
              // 控制进度
              onProgress({ loaded: 0, total: 100 });
              // 错误处理
              reject(xhr.statusText);
            }
          }
        };
        //  保存所有的 xhr
        requestList?.push(xhr);
      });
    },
    // 选择文件的时候会触发这个事件， e.target.files 表示选择的文件列表
    handleFileChange(e) {
      const [file] = e.target.files;
      if (!file) {
        this.file = null;
        return;
      }
      this.file = file;
    },
    // 生成文件切片
    createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        // file.slice 返回一个 blob对象
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    },
    // 控制并发数量   max表示最大的并发数
    async sendRequest(forms, max = 4) {
      return new Promise((resolve, reject) => {
        const len = forms.length;
        let counter = 0; // 已经发送成功的请求
        const retryArr = []; // 记录错误的次数
        // 一开始将所有的表单状态置为等待
        forms.forEach((item) => (item.status = Status.wait));
        const start = async () => {
          // 有请求，有通道
          while (counter < len && max > 0) {
            max--; // 占用通道
            // 只要是没有完成的我们就重发
            let idx = forms.findIndex((v) => v.status == Status.wait || v.status == Status.error);
            if (idx == -1) {
              // 找不到失败状态和等待状态
              return reject();
            }
            let { formData, index } = forms[idx];
            await this.request({
              url: 'http://localhost:8080/upload-chunk',
              method: 'post',
              data: formData,
              onProgress: this.createProgressHandler(this.chunkList[index]),
              requestList: this.requestList,
            })
              .then(() => {
                forms[idx].status = Status.done;
                max++; // 释放通道
                counter++;
                if (counter === len) {
                  resolve();
                }
              })
              .catch(() => {
                forms[idx].status = Status.error;
                if (typeof retryArr[index] !== 'number') {
                  this.$message.info(`第 ${index} 个块上传失败，系统准备重试`);
                  retryArr[index] = 0;
                }
                // 次数累加
                retryArr[index]++;
                // 一个请求报错3次的
                if (retryArr[index] > 3) {
                  this.$message.error(`第 ${index} 个块重试多次无效，放弃上传`);
                  forms[idx].status = Status.fail;
                }
                max++; // 释放通道
              });
          }
        };
        start();
      });
    },
    // 上传文件切片
    async uploadChunks(uploadedList = []) {
      // 构造请求列表
      const requestList = this.chunkList
        .filter((chunk) => !uploadedList.includes(chunk.chunkHash))
        .map(({ chunk, chunkHash, index, fileHash }) => {
          const formData = new FormData();
          formData.append('chunk', chunk);
          formData.append('chunkHash', chunkHash);
          formData.append('fileHash', fileHash);
          return { formData, index };
        });
      // .map(async ({ formData, index }) =>
      //   this.request({
      //     url: 'http://localhost:8080/upload-chunk',
      //     method: 'post',
      //     data: formData,
      //     onProgress: this.createProgressHandler(this.chunkList[index]),
      //     requestList: this.requestList,
      //   })
      // );
      // 等待全部发送完成
      // await Promise.all(requestList); // 并发切片
      // 控制并发
      await this.sendRequest(requestList, 4);
      // chunk 全部发送完成了需要通知后台去合并切片
      if (uploadedList.length + requestList.length === this.chunkList.length) {
        await this.mergeRequest();
      }
    },
    // 通知服务的合并切片
    async mergeRequest() {
      await this.request({
        url: 'http://localhost:8080/merge',
        method: 'post',
        headers: { 'content-type': 'application/json' },
        data: JSON.stringify({ filename: this.file.name, fileSize: this.file.size, size: SIZE, hash: this.hash }),
      });
    },
    //  上传按钮点击事件
    async handleUpload() {
      if (!this.file) {
        this.$message.info('请选择一个文件吧');
        return;
      }
      this.resetData();
      // 文件分片
      const fileChunkList = this.createFileChunk(this.file);
      // 计算文件hash
      this.hash = await this.calculateHash(fileChunkList);

      // 根据hash判断是否可以妙传
      const { shouldUpload, uploadedList } = await this.verifyUpload(this.file.name, this.hash);
      if (!shouldUpload) {
        this.$message.success('秒传：上传成功');
        return;
      }
      // 构建 chunkList  添加下标以及 上传进度(是每一个chunk的上传进度)
      this.chunkList = fileChunkList.map(({ file }, index) => ({
        chunk: file,
        size: file.size,
        chunkHash: `${this.hash}-${index}`,
        fileHash: this.hash,
        index,
        percentage: uploadedList.includes(`${this.hash}-${index}`) ? 100 : 0,
      }));
      // 上传 chunk
      await this.uploadChunks(uploadedList);
      this.$message.success('上传成功');
    },
    // 暂停
    handlePause() {
      this.requestList.forEach((xhr) => xhr?.abort());
      this.requestList = [];
      this.isPaused = false;
    },
    // 恢复
    async handleResume() {
      this.isPaused = true;
      const { uploadedList } = await this.verifyUpload(this.file.name, this.hash);
      await this.uploadChunks(uploadedList);
    },
    resetData() {
      this.chunkList = [];
      this.worker = null;
      this.hash = '';
      this.hashPercentage = 0;
      this.requestList = [];
      this.isPaused = true;
    },
  },
};
</script>

<style></style>
